import os
import tkinter as tk
from tkinter import filedialog, scrolledtext, messagebox, ttk
import ast
import difflib
import re

class FunctionInfo:
    def __init__(self, name, lineno, end_lineno, code, class_name=None):
        self.name = name
        self.lineno = lineno
        self.end_lineno = end_lineno
        self.code = code
        self.class_name = class_name  # 新增：函数所属类名
        self.is_used = False

    def __repr__(self):
        if self.class_name:
            return f"Method '{self.name}' of class '{self.class_name}' at line {self.lineno}-{self.end_lineno}"
        return f"Function '{self.name}' at line {self.lineno}-{self.end_lineno}"

class FunctionDuplicateChecker:
    def __init__(self, root):
        self.root = root
        self.root.title("Python 函数分析工具")
        self.root.geometry("900x600")
        self.root.minsize(800, 500)

        # 设置字体以确保中文正常显示
        default_font = ('SimHei', 10)

        # 创建文件选择区域
        file_frame = tk.Frame(root, padx=10, pady=10)
        file_frame.pack(fill=tk.X)

        tk.Label(file_frame, text="选择 Python 文件:", font=default_font).pack(side=tk.LEFT, padx=5)

        self.file_path_var = tk.StringVar()
        file_entry = tk.Entry(file_frame, textvariable=self.file_path_var, width=50, font=default_font)
        file_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)

        browse_btn = tk.Button(file_frame, text="浏览...", command=self.browse_file, font=default_font)
        browse_btn.pack(side=tk.LEFT, padx=5)

        # 创建功能选择标签页
        self.tab_control = ttk.Notebook(root)
        
        # 重复函数检测标签页
        self.duplicate_tab = ttk.Frame(self.tab_control)
        self.tab_control.add(self.duplicate_tab, text="重复函数检测")
        
        # 无用函数检测标签页
        self.unused_tab = ttk.Frame(self.tab_control)
        self.tab_control.add(self.unused_tab, text="无用函数检测")
        
        self.tab_control.pack(expand=1, fill="both")

        # 重复函数检测区域
        dup_frame = tk.Frame(self.duplicate_tab, padx=10, pady=10)
        dup_frame.pack(fill=tk.BOTH, expand=True)

        # 新增：选择检测模式
        mode_frame = tk.Frame(dup_frame)
        mode_frame.pack(fill=tk.X, pady=5)
        
        self.check_mode = tk.StringVar(value="all")
        tk.Radiobutton(mode_frame, text="全局重名函数", variable=self.check_mode, value="all", font=default_font).pack(side=tk.LEFT, padx=10)
        tk.Radiobutton(mode_frame, text="类内重名方法", variable=self.check_mode, value="class", font=default_font).pack(side=tk.LEFT, padx=10)

        check_dup_btn = tk.Button(dup_frame, text="检测重复函数", command=self.check_duplicates, 
                             bg="#4CAF50", fg="white", font=default_font)
        check_dup_btn.pack(anchor=tk.NW, pady=5)

        tk.Label(dup_frame, text="检测结果:", font=default_font).pack(anchor=tk.W)

        self.dup_result_text = scrolledtext.ScrolledText(dup_frame, wrap=tk.WORD, font=default_font)
        self.dup_result_text.pack(fill=tk.BOTH, expand=True, pady=5)

        # 无用函数检测区域
        unused_frame = tk.Frame(self.unused_tab, padx=10, pady=10)
        unused_frame.pack(fill=tk.BOTH, expand=True)

        check_unused_btn = tk.Button(unused_frame, text="检测无用函数", command=self.check_unused_functions, 
                             bg="#FF9800", fg="white", font=default_font)
        check_unused_btn.pack(anchor=tk.NW, pady=5)

        tk.Label(unused_frame, text="检测结果:", font=default_font).pack(anchor=tk.W)

        self.unused_result_text = scrolledtext.ScrolledText(unused_frame, wrap=tk.WORD, font=default_font)
        self.unused_result_text.pack(fill=tk.BOTH, expand=True, pady=5)

    def browse_file(self):
        file_path = filedialog.askopenfilename(
            title="选择 Python 文件",
            filetypes=[("Python 文件", "*.py"), ("所有文件", "*.*")]
        )
        if file_path:
            self.file_path_var.set(file_path)

    def check_duplicates(self):
        file_path = self.file_path_var.get()
        if not file_path:
            messagebox.showwarning("警告", "请选择一个 Python 文件")
            return

        if not os.path.isfile(file_path):
            messagebox.showerror("错误", f"文件不存在: {file_path}")
            return

        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
        except Exception as e:
            messagebox.showerror("错误", f"读取文件失败: {str(e)}")
            return

        try:
            tree = ast.parse(content)
        except SyntaxError as e:
            messagebox.showerror("错误", f"解析 Python 代码失败: {str(e)}")
            return

        # 提取所有函数和方法定义
        functions = []
        for node in ast.walk(tree):
            if isinstance(node, ast.ClassDef):
                class_name = node.name
                # 提取类中的方法
                for sub_node in node.body:
                    if isinstance(sub_node, ast.FunctionDef):
                        start_lineno = sub_node.lineno
                        end_lineno = self._get_end_lineno(sub_node, content)
                        function_code = self._extract_function_code(content, start_lineno, end_lineno)
                        functions.append(FunctionInfo(sub_node.name, start_lineno, end_lineno, function_code, class_name))
            elif isinstance(node, ast.FunctionDef):
                # 提取全局函数
                start_lineno = node.lineno
                end_lineno = self._get_end_lineno(node, content)
                function_code = self._extract_function_code(content, start_lineno, end_lineno)
                functions.append(FunctionInfo(node.name, start_lineno, end_lineno, function_code))

        # 根据选择的模式进行分组
        check_mode = self.check_mode.get()
        
        if check_mode == "all":
            # 全局重名函数检测
            function_groups = {}
            for func in functions:
                key = func.name  # 只根据函数名分组
                if key in function_groups:
                    function_groups[key].append(func)
                else:
                    function_groups[key] = [func]
            title = "全局重名函数检测结果"
            desc = "以下函数在全局范围内存在重名（不考虑是否在同一个类中）"
            
        else:  # check_mode == "class"
            # 类内重名方法检测
            function_groups = {}
            for func in functions:
                if func.class_name:  # 只考虑类方法，不考虑全局函数
                    key = (func.class_name, func.name)  # 根据类名和函数名分组
                    if key in function_groups:
                        function_groups[key].append(func)
                    else:
                        function_groups[key] = [func]
            title = "类内重名方法检测结果"
            desc = "以下方法在同一个类中存在重名"

        # 找出所有重复的函数/方法
        duplicate_functions = {name: funcs for name, funcs in function_groups.items() if len(funcs) > 1}

        # 显示结果
        self.dup_result_text.delete(1.0, tk.END)
        if not duplicate_functions:
            self.dup_result_text.insert(tk.END, f"恭喜！没有发现{title.split('结果')[0]}。\n")
            return

        self.dup_result_text.insert(tk.END, f"{title}:\n\n{desc}:\n\n")
        
        for name, funcs in duplicate_functions.items():
            if check_mode == "class":
                class_name, func_name = name
                self.dup_result_text.insert(tk.END, f"类名: {class_name}\n", 'class_heading')
                self.dup_result_text.insert(tk.END, f"方法名: {func_name}\n", 'method_heading')
            else:
                func_name = name
                self.dup_result_text.insert(tk.END, f"函数名: {func_name}\n", 'heading')
            
            self.dup_result_text.insert(tk.END, f"出现次数: {len(funcs)}\n", 'subheading')
            
            for i, func in enumerate(funcs):
                location = f"  位置 {i+1}: 第 {func.lineno}-{func.end_lineno} 行"
                if func.class_name:
                    location += f" (类 '{func.class_name}')"
                self.dup_result_text.insert(tk.END, f"{location}\n", 'location')
            
            # 比较函数体相似度
            self.dup_result_text.insert(tk.END, "\n  函数体相似度比较:\n", 'subheading')
            for i in range(len(funcs)):
                for j in range(i+1, len(funcs)):
                    similarity = self._calculate_similarity(funcs[i].code, funcs[j].code)
                    self.dup_result_text.insert(tk.END, f"  - 位置 {i+1} 和位置 {j+1} 的相似度: {similarity:.2f}%\n")
                    
                    # 如果相似度低于阈值，显示差异
                    if similarity < 90:
                        self.dup_result_text.insert(tk.END, f"    差异分析:\n", 'diff_heading')
                        diff = self._generate_diff(funcs[i].code, funcs[j].code)
                        self.dup_result_text.insert(tk.END, f"    {diff}\n", 'diff')
            
            self.dup_result_text.insert(tk.END, "\n" + "-"*80 + "\n\n")

        # 配置文本标签样式
        self.dup_result_text.tag_config('heading', font=('SimHei', 12, 'bold'))
        self.dup_result_text.tag_config('class_heading', font=('SimHei', 12, 'bold'), foreground='blue')
        self.dup_result_text.tag_config('method_heading', font=('SimHei', 11, 'bold'))
        self.dup_result_text.tag_config('subheading', font=('SimHei', 10, 'bold'))
        self.dup_result_text.tag_config('location', foreground='blue')
        self.dup_result_text.tag_config('diff_heading', foreground='green')
        self.dup_result_text.tag_config('diff', foreground='red')

    def check_unused_functions(self):
        file_path = self.file_path_var.get()
        if not file_path:
            messagebox.showwarning("警告", "请选择一个 Python 文件")
            return

        if not os.path.isfile(file_path):
            messagebox.showerror("错误", f"文件不存在: {file_path}")
            return

        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
        except Exception as e:
            messagebox.showerror("错误", f"读取文件失败: {str(e)}")
            return

        try:
            tree = ast.parse(content)
        except SyntaxError as e:
            messagebox.showerror("错误", f"解析 Python 代码失败: {str(e)}")
            return

        # 提取所有函数和方法定义
        functions = []
        for node in ast.walk(tree):
            if isinstance(node, ast.ClassDef):
                class_name = node.name
                # 提取类中的方法
                for sub_node in node.body:
                    if isinstance(sub_node, ast.FunctionDef):
                        start_lineno = sub_node.lineno
                        end_lineno = self._get_end_lineno(sub_node, content)
                        function_code = self._extract_function_code(content, start_lineno, end_lineno)
                        functions.append(FunctionInfo(sub_node.name, start_lineno, end_lineno, function_code, class_name))
            elif isinstance(node, ast.FunctionDef):
                # 提取全局函数
                start_lineno = node.lineno
                end_lineno = self._get_end_lineno(node, content)
                function_code = self._extract_function_code(content, start_lineno, end_lineno)
                functions.append(FunctionInfo(node.name, start_lineno, end_lineno, function_code))

        # 分析函数调用
        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Name):
                    func_name = node.func.id
                    # 检查是否是我们分析的函数之一
                    for func in functions:
                        if func.name == func_name and not func.class_name:  # 全局函数
                            func.is_used = True
                elif isinstance(node.func, ast.Attribute):
                    # 处理 method calls (e.g., obj.method())
                    if isinstance(node.func.value, ast.Name):
                        obj_name = node.func.value.id
                        method_name = node.func.attr
                        # 简化处理：假设obj是类的实例，method_name是类的方法
                        for func in functions:
                            if func.class_name and func.name == method_name:
                                func.is_used = True

        # 找出未使用的函数
        unused_functions = [func for func in functions if not func.is_used]

        # 显示结果
        self.unused_result_text.delete(1.0, tk.END)
        if not unused_functions:
            self.unused_result_text.insert(tk.END, "恭喜！没有发现未使用的函数。\n")
            return

        self.unused_result_text.insert(tk.END, f"发现 {len(unused_functions)} 个未使用的函数:\n\n")
        
        for i, func in enumerate(unused_functions):
            if func.class_name:
                self.unused_result_text.insert(tk.END, f"{i+1}. 类 '{func.class_name}' 的方法: {func.name}\n", 'heading')
            else:
                self.unused_result_text.insert(tk.END, f"{i+1}. 全局函数: {func.name}\n", 'heading')
            
            self.unused_result_text.insert(tk.END, f"   位置: 第 {func.lineno}-{func.end_lineno} 行\n", 'location')
            self.unused_result_text.insert(tk.END, f"   函数体行数: {len(func.code.splitlines())}\n", 'info')
            
            # 显示函数体的前几行
            code_lines = func.code.splitlines()
            preview = '\n'.join(code_lines[:3]) + ('...' if len(code_lines) > 3 else '')
            self.unused_result_text.insert(tk.END, f"   函数体预览:\n     {preview.replace('\n', '\n     ')}\n", 'code')
            
            self.unused_result_text.insert(tk.END, "\n" + "-"*60 + "\n\n")

        # 配置文本标签样式
        self.unused_result_text.tag_config('heading', font=('SimHei', 12, 'bold'))
        self.unused_result_text.tag_config('location', foreground='blue')
        self.unused_result_text.tag_config('info', foreground='green')
        self.unused_result_text.tag_config('code', foreground='black', font=('Consolas', 9))

    def _get_end_lineno(self, node, content):
        """获取函数定义的结束行号"""
        lines = content.splitlines()
        start_lineno = node.lineno
        indentation = self._get_indentation(lines[start_lineno-1])
        
        # 从函数定义开始行之后的行开始查找
        for i in range(start_lineno, len(lines) + 1):
            # 如果是最后一行，返回当前行
            if i == len(lines):
                return i
            
            # 获取当前行的缩进
            current_indent = self._get_indentation(lines[i])
            
            # 如果当前行缩进小于等于函数定义行的缩进，说明函数体结束
            if current_indent <= indentation:
                return i
        
        return len(lines)

    def _get_indentation(self, line):
        """获取行的缩进空格数"""
        return len(line) - len(line.lstrip())

    def _extract_function_code(self, content, start_lineno, end_lineno):
        """提取函数代码"""
        lines = content.splitlines()
        return '\n'.join(lines[start_lineno-1:end_lineno])

    def _calculate_similarity(self, code1, code2):
        """计算两段代码的相似度"""
        # 移除注释和空白字符以进行更准确的比较
        code1_clean = self._clean_code(code1)
        code2_clean = self._clean_code(code2)
        
        if not code1_clean and not code2_clean:
            return 100.0
        
        if not code1_clean or not code2_clean:
            return 0.0
        
        seq_matcher = difflib.SequenceMatcher(None, code1_clean, code2_clean)
        return seq_matcher.ratio() * 100

    def _clean_code(self, code):
        """清理代码，移除注释和空白字符"""
        # 移除行注释
        code = re.sub(r'#.*$', '', code, flags=re.M)
        
        # 移除块注释
        code = re.sub(r"'''.*?'''", '', code, flags=re.DOTALL)
        code = re.sub(r'""".*?"""', '', code, flags=re.DOTALL)
        
        # 移除空白行和前后空白字符
        lines = [line.strip() for line in code.splitlines() if line.strip()]
        return '\n'.join(lines)

    def _generate_diff(self, code1, code2):
        """生成两个代码块的差异"""
        code1_lines = code1.splitlines()
        code2_lines = code2.splitlines()
        
        diff = difflib.unified_diff(
            code1_lines, code2_lines, 
            fromfile='位置1', tofile='位置2', 
            lineterm=''
        )
        
        # 只返回前几行差异以避免过多输出
        diff_lines = list(diff)
        return '\n    '.join(diff_lines[:10]) + ('...' if len(diff_lines) > 10 else '')

if __name__ == "__main__":
    root = tk.Tk()
    app = FunctionDuplicateChecker(root)
    root.mainloop() 